﻿/*
	Copyright (C) 2011-2012 Bernardo van der Wal <bernardo@thebigb.nl>

	Permission is hereby granted, free of charge, to any person obtaining a copy of
	this software and associated documentation files (the "Software"), to deal in
	the Software without restriction, including without limitation the rights to
	use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
	of the Software, and to permit persons to whom the Software is furnished to do
	so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/
using System;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace TheBigB.JsonSerializerLite
{
	/// 
	/// Lightweight JSON serializer.
	/// 
	/// Usable methods:
	/// 
	///		[TODO]
	///    
	/// Thread safety:
	///		These methods are not thread-safe. Measures have been taken to make the
	///		caches thread-safe, but seperate instances should be spawned for each 
	///		using thread.
	/// 
	/// Changelog:
	///		2012-07-30
	///			- Changed Assembly.CreateInstance to Activator.CreateInstance for
	///			  better performance
	///			- Strings and nested objects can be deserialized (not unescaped 
	///			  though)
	///		
	///		2012-09-01
	///			- Now we have support most value types (still unsure about structs)
	///			
	///		2012-09-07
	///			- Strings unescaping is functional
	///			- Added unescaping for unicode characters
	///			
	///		2012-09-16
	///			- Finished extension functionality for deserializer
	///			
	/// TODOS: 
	///		- Document usable methods above
	///		- Deserialize arrays
	///		- More builtin extensions, especially for Stack, Queue, List etc.
	///

	/// <summary>
	/// Lightweight JSON serializer.
	/// </summary>
	public partial class JsonSerializer
	{
		#region ... Non-public static fields ...

		/// <summary>
		/// Cache for string key unescaping.
		/// This cache is used for field names, to prevent overhead with repeated usage of the same types.
		/// </summary>
		protected static Cache<string, string> _keyUnescapeCache = new Cache<string, string>(MAX_KEY_CACHE);

		/// <summary>
		/// Cache for string value unescaping.
		/// This cache is used for field values, to prevent overhead with repeated usage of the same data.
		/// </summary>
		protected static Cache<string, string> _valueUnescapeCache = new Cache<string, string>(MAX_VALUE_CACHE);

		#endregion

		#region ... Public static fields ...

		/// <summary>
		/// Gets or sets the maximum cache size for object key escaping.
		/// When set to '0' an the maximum size is unlimited.
		/// </summary>
		public static int KeyUnescapeCacheSize
		{
			get { return _keyUnescapeCache.MaxSize; }
			set { _keyUnescapeCache.MaxSize = value; }
		}

		/// <summary>
		/// Gets or sets the maximum cache size for object value escaping.
		/// When set to '0' an the maximum size is unlimited.
		/// </summary>
		public static int ValueUnescapeCacheSize
		{
			get { return _valueUnescapeCache.MaxSize; }
			set { _valueUnescapeCache.MaxSize = value; }
		}

		#endregion

		#region ... Embedded classes and enums ...

		/// <summary>
		/// Wrapper class to hold a modifyable JsonStates enum
		/// in a stack.
		/// </summary>
		protected class JsonState
		{
			/// <summary>
			/// Gets or sets the state for this object.
			/// </summary>
			public JsonStates State { get; set; }

			/// <summary>
			/// Initialize an instance of this class without a default value.
			/// State initializes as JsonStates.Key.
			/// </summary>
			public JsonState() { }

			/// <summary>
			/// Initialize an instance of this class with the provided value.
			/// </summary>
			/// <param name="initial">Initial value to set</param>
			public JsonState(JsonStates initial)
			{
				this.State = initial;
			}
		}

		/// <summary>
		/// Representation of states while deserializing
		/// </summary>
		protected enum JsonStates
		{
			/// <summary>
			/// We are currently parsing a key
			/// </summary>
			Key,
			/// <summary>
			/// We are currently parsing a value
			/// </summary>
			Value,
			/// <summary>
			/// We are currently parsing an array
			/// </summary>
			Array,
			/// <summary>
			/// We are currently buffering a JSON part for an extension
			/// </summary>
			Extension,
			/// <summary>
			/// We are currently buffering a JSON descendant for an extension
			/// </summary>
			ExtensionDescendant
		}

		#endregion

		#region ... Public methods ...

		/// <summary>
		/// Returns a populated object for the provided JSON string.
		/// </summary>
		/// <typeparam name="T">Type of object to serialize</typeparam>
		/// <param name="obj">Object to serialize</param>
		/// <returns>JSON formatted string</returns>
		public T Deserialize<T>(string jsonString)
		{
			if (String.IsNullOrEmpty(jsonString))
				return default(T);

			bool inString = false;
			char prev = ' ';

			Stack<JsonState> stateStack = new Stack<JsonState>();
			Stack<PropertyInfo> propertyStack = new Stack<PropertyInfo>();
			Stack objStack = new Stack();

			PropertyInfo property = null;
			string keyBuffer = "";
			object valueBuffer = null;
			string extensionBuffer = "";

			foreach (char c in jsonString)
			{

				// We buffer characters for extensions here to make sure we catch 
				// all the characters
				if (stateStack.Count > 0 && (stateStack.Peek().State == JsonStates.Extension || stateStack.Peek().State == JsonStates.ExtensionDescendant))
					extensionBuffer += c;

				if (c == '"' && prev != '\\')
				{
					// The state stack being empty indicates that the object hasn't
					// been opened yet; so we can't parse strings yet.
					if (stateStack.Count == 0)
						continue;

					// Toggle 'inString' if we find a double-quote that hasn't 
					// been escaped.
					inString = !inString;
					prev = c;
					continue;
				}

				if (inString)
				{
					// If we are 'inString', add the current character to either 
					// the key or the value buffer (depending on the state).
					switch (stateStack.Peek().State)
					{
						case JsonStates.Key: keyBuffer += c; break;
						case JsonStates.Value: valueBuffer = (string)valueBuffer + c; break;
					}
				}
				else
				{
					// If we are outside a string, we check for control characters or
					// another specific set of characters that are allowed to appear
					// outside string context.
					if (!this.isControlChar(c) && !this.allowedOutsideString(c))
						continue;

					if (stateStack.Count == 0)
					{
						// The state-stack being empty means we are yet to enter the
						// first object in the JSON string.
						if (c == '{')
						{
							// When we encounter the first object-open character, we initialize the
							// state stack and create an instance of T on the object stack

							stateStack.Push(new JsonState());
							objStack.Push(createInstance(typeof(T)));
						}
					}
					else
					{
						switch (stateStack.Peek().State)
						{
							// Out of string context and at key-state now.
							// We expect a colon to introduce the value
							case JsonStates.Key:

								if (c == ':')
								{
									// Get the PropertyInfo for later use and push on a stack in
									// case we encounter nested objects
									property = this.getProperty(keyBuffer, objStack.Peek());
									propertyStack.Push(property);
									
									// Set state to Extension if the property's type was registered
									// with an extension.
									if (property == null || !this.Extenders.HasExtension(property.PropertyType, true))
									{
										stateStack.Peek().State = JsonStates.Value;
									}
									else
									{
										stateStack.Peek().State = JsonStates.Extension;
										extensionBuffer = "";
									}

									// Reset key buffer
									keyBuffer = "";
								}

								break;

							// Out of string context and at value-state now.
							// We expect a JSON formatted number, boolean value, null,
							// string or a child object.
							case JsonStates.Value:

								if (c == ',' || c == '}')
								{
									// We either switch to the next property or close the current object.
									// This means that we have to prepare the buffered value to be assigned
									// to the object's property.

									property = propertyStack.Pop();
									if(property != null)
									{
										// Deserialize (and possibly unescape) the value
										valueBuffer = this.DeserializeValue(valueBuffer, property.PropertyType);
										// Assign to the object on top of the stack
										property.SetValue(objStack.Peek(), valueBuffer, null);
									}

									// Reset the value buffer and reset the state to Key
									valueBuffer = null;
									stateStack.Peek().State = JsonStates.Key;
								}

								if (c == '{')
								{
									// A nested object was opened. Create an instance of its type and push
									// it onto the object stack. Also push a new state onto the stack.
									objStack.Push(createInstance(propertyStack.Peek().PropertyType));
									stateStack.Push(new JsonState());
								}

								if (c == '}')
								{
									// Close object. Put the object we have accumulated in the value
									// buffer and revert to the state we were before this object was
									// started.
									valueBuffer = objStack.Pop();
									stateStack.Pop();
								}

								// If the current character is not a control character, add it to the
								// value buffer.
								if (!this.isControlChar(c))
									valueBuffer = (string)valueBuffer + c;

								break;

							case JsonStates.Extension:

								if (c == '{')
									stateStack.Push(new JsonState(JsonStates.ExtensionDescendant));

								if (c == ',' || c == '}')
								{
									// We either switch to the next property or close the current object.
									// This means that we have to prepare the buffered value to be assigned
									// to the object's property.

									property = propertyStack.Pop();
									if (property != null)
									{
										// Let extension deserialize the buffered JSON text
										IJsonSerializerExtension extender = this._extenders.GetExtension(property.PropertyType);

										extensionBuffer = extensionBuffer.Substring(0, extensionBuffer.Length - 1);
										valueBuffer = extender.Deserialize(extensionBuffer, property.PropertyType, this);

										// Assign to the object on top of the stack
										property.SetValue(objStack.Peek(), valueBuffer, null);
									}

									// Reset the value buffer and reset the state to Key
									valueBuffer = null;
									stateStack.Peek().State = JsonStates.Key;
								}

								if (c == '}')
								{
									// Close object. Put the object we have accumulated in the value
									// buffer and revert to the state we were before this object was
									// started.
									valueBuffer = objStack.Pop();
									stateStack.Pop();
								}

								break;

							case JsonStates.ExtensionDescendant:
								
								if (c == '{')
									stateStack.Push(new JsonState(JsonStates.ExtensionDescendant));

								if (c == '}')
									stateStack.Pop();

								break;
						}
					}

					// Record the current character as last character.
					// This is only used to check whether the last character was
					// an escape character.
					prev = c;
				}
			}

			return (T)valueBuffer;
		}

		/// <summary>
		/// Converts the provided value into it's appropriate type. If the type is
		/// unsupported, the provided value is returned unchanged.
		/// This method also takes care of unescaping strings.
		/// </summary>
		/// <param name="value">Object to convert</param>
		/// <param name="type">Type to convert it to</param>
		/// <returns>Typed object</returns>
		public object DeserializeValue(object value, Type type)
		{
			if (value == null || value.ToString() == "null")
				return null;

			switch (Type.GetTypeCode(type))
			{
				// Unescape and return the first string character
				case TypeCode.Char:
					return this.UnescapeString(value.ToString())[0];
				
				// Return unescaped string
				case TypeCode.String: return this.UnescapeString(value.ToString());

				// Return type class parsed versions
				case TypeCode.Boolean:
				case TypeCode.Byte:
				case TypeCode.SByte:
				case TypeCode.Int16:
				case TypeCode.Int32:
				case TypeCode.Int64:
				case TypeCode.UInt16:
				case TypeCode.UInt32:
				case TypeCode.UInt64:
				case TypeCode.Decimal:
				case TypeCode.Single:
				case TypeCode.Double:
					return Convert.ChangeType(value, type, _jsonNumberFormat);

				// For unsupported types return the object itself
				default:
					return value;
			}
		}

		/// <summary>
		/// Turns escaped characters into regular control or unicode characters.
		/// The isKey paramater is used for caching purposes as the key-cache is likely to be smaller than the value-cache.
		/// </summary>
		/// <param name="str">String to unescape</param>
		/// <param name="isKey">Specifies whether the string is a JSON key (for caching efficiency)</param>
		/// <returns>Unescaped string</returns>
		public string UnescapeString(string str, bool isKey = false)
		{
			if (str == null)
				return "";

			string buffer = "";

			// Check whether the input string already has a processed string in cache
			if (isKey)
			{
				if (_keyUnescapeCache.TryGet(str, out buffer))
					return buffer;
			}
			else
			{
				if (_valueUnescapeCache.TryGet(str, out buffer))
					return buffer;
			}

			// Loop through each character and escape if nessecary
			for (int i = 0; i < str.Length; i++)
			{
				char c = str[i];
				if (c == '\\')
				{
					switch (str[i + 1])
					{
						case '"': buffer += "\""; i += 1; break;
						case '\\': buffer += "\\"; i += 1; break;
						case '/': buffer += "/"; i += 1; break;
						case 'b': buffer += "\b"; i += 1; break;
						case 'f': buffer += "\f"; i += 1; break;
						case 'n': buffer += "\n"; i += 1; break;
						case 'r': buffer += "\r"; i += 1; break;
						case 't': buffer += "\t"; i += 1; break;
						case 'u':
							// '\uXXXX' represent serialized unicode character
							string hexBuffer = "";
							for (int u = 2; u < 6; u++)
							{
								if (!this.isHex(str[i + u]))
									break;

								// Implicit else
								hexBuffer += str[i + u];
							}

							// Convert hexadecimal characters to its unicode representation
							buffer += this.hexToUnicodeChar(hexBuffer);
							i += 5;
							break;
						default:
							buffer += str[i];
							break;
					}
				}
				else
				{
					buffer += str[i];
				}
			}

			// Determine cache type
			if (isKey)
				_keyUnescapeCache.Add(str, buffer);
			else
				_valueUnescapeCache.Add(str, buffer);

			return buffer;
		}

		#endregion

		#region ... Non-public methods ...

		/// <summary>
		/// Checks whether or not the provided character is allowed to appear outside a 
		/// string context. These are the characters that form 'true', 'false', 'null' and
		/// the format of numbers (+-.E for doubles).
		/// </summary>
		/// <param name="c">Character to verify</param>
		/// <returns>Allowed outside string?</returns>
		protected bool allowedOutsideString(char c)
		{
			return (
				c == 'a' || c == 'e' || c == 'f' || c == 'l' || c == 'n' || c == 'r' || 
				c == 's' || c == 't' || c == 'u' || c == 'E' || c == '.' || c == '+' || 
				c == '-' || Char.IsDigit(c)
			);
		}

		/// <summary>
		/// Checks whether the provided character is a control character in the JSON 
		/// syntax.
		/// </summary>
		/// <param name="c">Character to verify</param>
		/// <returns>Is control character?</returns>
		protected bool isControlChar(char c)
		{
			return (c == ',' || c == ':' || c == '{' || c == '}' || c == '[' || c == ']');
		}

		/// <summary>
		/// Checks whether the provided character is of hexadecimal representation
		/// </summary>
		/// <param name="c">Character to verify</param>
		/// <returns>Is hexadecimal character?</returns>
		protected bool isHex(char c)
		{
			return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
		}

		/// <summary>
		/// Converts a 4 character hex string to a character
		/// </summary>
		/// <param name="hex">String to convert</param>
		/// <returns>Character representation</returns>
		protected char hexToUnicodeChar(string hex)
		{
			if (hex.Length != 4)
				throw new ArgumentException("Invalid input length");

			byte[] byteBuffer = new byte[2];
			byteBuffer[0] = Convert.ToByte(hex.Substring(0, 2), 16);
			byteBuffer[1] = Convert.ToByte(hex.Substring(2, 2), 16);

			char[] charBuffer = Encoding.Unicode.GetChars(byteBuffer);
			return charBuffer[0];
		}

		/// <summary>
		/// Little wrapper with failsafe to retrieve property information by name
		/// </summary>
		/// <param name="name">Name of the property to retrieve</param>
		/// <param name="obj">Subject</param>
		/// <returns>Property information or null</returns>
		protected PropertyInfo getProperty(string name, object obj)
		{
			if (obj == null || String.IsNullOrEmpty(name))
				return null;

			return obj.GetType().GetProperty(name);
		}

		/// <summary>
		/// Wrapper to create an instance of the specified type
		/// </summary>
		/// <param name="type">Type to create instance of</param>
		/// <returns>Instance of given type</returns>
		protected object createInstance(Type type)
		{
			if (type == null)
				return null;

			try
			{
				return Activator.CreateInstance(type);
			}
			catch (Exception e)
			{
				throw new InstanceCreateException(String.Format("Failed while trying to create an instance of type: {0}", type.FullName), e);
			}
		}

		#endregion
	}
}
